fix(tools): propagate tool registry to subagents#1711
Conversation
SubagentManager was created with an empty ToolRegistry and SetTools() was never called, causing all subagent tool invocations to fail with "tool not found". This was a regression from the multi-agent refactor. Fix: clone the parent agent's tool registry into the subagent manager after creation but before spawn/spawn_status registration — giving subagents access to file, exec, web, and other tools while preventing recursive subagent spawning. - Add ToolRegistry.Clone() for independent shallow copies - Call subagentManager.SetTools(agent.Tools.Clone()) in registerSharedTools - Add tests for Clone isolation, empty clone, and hidden tool state Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
yinwm
left a comment
There was a problem hiding this comment.
Code Review
This is a critical bugfix that resolves the issue where subagents were completely non-functional due to missing tool propagation. The changes are minimal and well-targeted.
✅ What's Good
-
Accurate root cause analysis - Correctly identified that
SubagentManagerwas created with an emptyToolRegistryandSetTools()was never called after the multi-agent refactor. -
Correct solution - Using
Clone()to create a snapshot rather than sharing the pointer effectively prevents recursive subagent spawning while giving subagents access to all necessary tools. -
Proper lock usage - Using
RLock()instead ofLock()since we're only reading. -
Performance optimization - Pre-allocating the map with
make(map[string]*ToolEntry, len(r.tools))avoids multiple allocations. -
Good test coverage - Tests verify bidirectional isolation, edge case (empty clone), and hidden tool state preservation.
⚠️ Items to Consider
1. version field not copied
clone := &ToolRegistry{
tools: make(map[string]*ToolEntry, len(r.tools)),
// version field defaults to 0
}The version field in ToolRegistry is used for cache invalidation. The clone starts with version=0, which should be fine (the clone is a new independent registry managing its own version). However, it might be worth adding a note in the comment explaining this behavior.
2. Test doesn't verify TTL value is correctly copied
The current test TestToolRegistry_Clone_PreservesHiddenToolState only checks TTL=0 case. Consider adding a test to verify TTL > 0 values are correctly preserved:
func TestToolRegistry_Clone_PreservesTTL(t *testing.T) {
r := NewToolRegistry()
tool := newMockTool("hidden_tool", "hidden")
r.RegisterHidden(tool)
// Manually set TTL > 0
if entry, ok := r.tools["hidden_tool"]; ok {
entry.TTL = 5
}
clone := r.Clone()
if entry, ok := clone.tools["hidden_tool"]; ok {
if entry.TTL != 5 {
t.Errorf("expected TTL=5, got %d", entry.TTL)
}
}
}💡 Minor Suggestion (Non-blocking)
The comment could mention that version is reset:
// Clone creates an independent copy of the registry containing the same tool
// entries (shallow copy of each ToolEntry). This is used to give subagents a
// snapshot of the parent agent's tools without sharing the same registry —
// tools registered on the parent after cloning (e.g. spawn, spawn_status)
// will NOT be visible to the clone, preventing recursive subagent spawning.
// The version counter is reset to 0 in the clone as it's a new independent registry.🎯 Verdict
| Aspect | Rating |
|---|---|
| Correctness | ✅ Fix logic is correct |
| Thread Safety | ✅ No concurrency issues |
| Maintainability | ✅ Code is clear |
| Test Coverage |
LGTM with minor nits — Ready to merge, but recommend adding a test case for TTL value preservation for completeness.
🤖 Generated with Claude Code
|
plz fix Linter and tests @paoloanzn |
- Fix cron_test.go:229 — replace non-existent SubscribeOutbound(ctx) with select on OutboundChan(), matching the MessageBus channel API - Add TestToolRegistry_Clone_PreservesTTLValue per reviewer feedback - Add version reset note to Clone() doc comment Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
@yinwm Thanks for the review! I've pushed a follow-up commit addressing both the CI failures and your review feedback: Fixes:
All tests pass locally ( CI may need approval to re-run since this is a fork PR. |
|
@paoloanzn sorry , there's a conflicts, plz fix |
88b1ea4 to
8851313
Compare
|
thanks for this pr |
my pleasure to contribute. best agent system out there rn 😄 |
|
@paoloanzn Great debugging work here. Tracking down the empty ToolRegistry after the multi-agent refactor and using Clone() to snapshot the parent tools while preventing recursive spawning is a well thought out solution. The isolation tests are thorough too. We have a PicoClaw Dev Group on Discord for contributors to collaborate more directly. Want to join? Send an email to |
thank you for the invite, i've sent the email! |
* fix(tools): propagate tool registry to subagents via Clone SubagentManager was created with an empty ToolRegistry and SetTools() was never called, causing all subagent tool invocations to fail with "tool not found". This was a regression from the multi-agent refactor. Fix: clone the parent agent's tool registry into the subagent manager after creation but before spawn/spawn_status registration — giving subagents access to file, exec, web, and other tools while preventing recursive subagent spawning. - Add ToolRegistry.Clone() for independent shallow copies - Call subagentManager.SetTools(agent.Tools.Clone()) in registerSharedTools - Add tests for Clone isolation, empty clone, and hidden tool state Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix(tools): fix cron_test build error and add TTL clone test - Fix cron_test.go:229 — replace non-existent SubscribeOutbound(ctx) with select on OutboundChan(), matching the MessageBus channel API - Add TestToolRegistry_Clone_PreservesTTLValue per reviewer feedback - Add version reset note to Clone() doc comment Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Summary
SubagentManageris created with an emptyToolRegistry(NewToolRegistry()atsubagent.go:48) andSetTools()is never called after the multi-agent refactor inregisterSharedTools(). This causes every tool invocation from a subagent to return"tool not found", making subagents completely non-functional.registerSharedTools()was introduced, thesubagentManager.SetTools()call that populated the subagent's tools was dropped. TheSetTools()andRegisterTool()methods exist onSubagentManagerbut are never invoked.ToolRegistry.Clone()to create an independent snapshot of the parent agent's tool registry, then callsubagentManager.SetTools(agent.Tools.Clone())immediately after manager creation but beforespawn/spawn_statusregistration — giving subagents access to file, exec, web, and other tools while preventing recursive subagent spawning.Changes
pkg/tools/registry.goClone()method — creates independent shallow copy of a tool registrypkg/agent/loop.gosubagentManager.SetTools(agent.Tools.Clone())inregisterSharedTools()pkg/tools/registry_test.goWhy Clone instead of sharing the pointer?
spawnandspawn_statustools are registered onagent.Toolsafter the subagent manager is created (lines 248-251 ofloop.go). Sharing the registry pointer would give subagents access to spawn tools → recursive subagent spawning.Clone()captures a snapshot with only the tools registered at that point.Test plan
TestToolRegistry_Clone— verifies parent/clone isolation in both directionsTestToolRegistry_Clone_Empty— verifies cloning an empty registryTestToolRegistry_Clone_PreservesHiddenToolState— verifies hidden tool TTL behavior is preservedpkg/toolsandpkg/agentpackages build cleanly🤖 Generated with Claude Code